/*
 *    GeoTools - The Open Source Java GIS Toolkit
 *    http://geotools.org
 *
 *    (C) 2005-2008, Open Source Geospatial Foundation (OSGeo)
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation;
 *    version 2.1 of the License.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 */
package org.geotools.filter.function;

// this was autogenerated and then hand modified to implement better support for geometry
// transformations in SLD

import static org.geotools.filter.capability.FunctionNameImpl.parameter;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.geotools.api.filter.capability.FunctionName;
import org.geotools.filter.FunctionExpressionImpl;
import org.geotools.filter.capability.FunctionNameImpl;
import org.geotools.filter.function.FilterFunction_offset.OffsetOrdinateFilter;
import org.geotools.geometry.jts.JTS;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.locationtech.jts.geom.CoordinateSequence;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryCollection;
import org.locationtech.jts.geom.GeometryComponentFilter;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.Polygon;

public class FilterFunction_isometric extends FunctionExpressionImpl
        implements GeometryTransformation {

    public static FunctionName NAME =
            new FunctionNameImpl(
                    "isometric",
                    Geometry.class,
                    parameter("geometry", Geometry.class),
                    parameter("extrusion", Double.class));

    public FilterFunction_isometric() {
        super(NAME);
    }

    @Override
    public Object evaluate(Object feature) {
        Geometry geom = getExpression(0).evaluate(feature, Geometry.class);
        Double extrusion = getExpression(1).evaluate(feature, Double.class);

        if (geom != null && extrusion != null) {
            // build the vertical faces
            SegmentExtractorFilter extractor = new SegmentExtractorFilter();
            geom.apply(extractor);
            List<Polygon> faces = extractor.getFaces(geom.getFactory(), extrusion);

            // add the "cap"
            if (geom instanceof Polygon) {
                Polygon offseted = (Polygon) geom.copy();
                offseted.apply(new OffsetOrdinateFilter(0, extrusion));
                faces.add(0, (Polygon) geom);
                faces.add(offseted);
            } else if (geom instanceof GeometryCollection) {
                GeometryCollection gc = (GeometryCollection) geom;
                for (int i = 0; i < gc.getNumGeometries(); i++) {
                    Geometry g = gc.getGeometryN(i);
                    if (g instanceof Polygon) {
                        Polygon offseted = (Polygon) g.copy();
                        offseted.apply(new OffsetOrdinateFilter(0, extrusion));
                        faces.add(0, (Polygon) g);
                        faces.add(offseted);
                    }
                }
            }

            Polygon[] polyArray = faces.toArray(new Polygon[faces.size()]);
            return geom.getFactory().createMultiPolygon(polyArray);

        } else {
            return null;
        }
    }

    /**
     * Returns an translated rendering envelope if the offsets are not using feature attributes. If
     * the offsets are feature dependent the user will have to expand the rendering area via the
     * renderer buffer parameter
     */
    @Override
    public ReferencedEnvelope invert(ReferencedEnvelope renderingEnvelope) {
        Double offsetY = getExpression(1).evaluate(null, Double.class);

        if (offsetY != null) {
            ReferencedEnvelope offseted = new ReferencedEnvelope(renderingEnvelope);
            offseted.translate(0, offsetY);
            return offseted;
        } else {
            return null;
        }
    }

    /** Extracts Segment objects out of the Geometry coordinate sequences */
    static class SegmentExtractorFilter implements GeometryComponentFilter {
        List<Segment> segments = new ArrayList<>();

        @Override
        public void filter(Geometry geom) {
            if (geom instanceof LineString) {
                extractSegments(((LineString) geom).getCoordinateSequence());
            }
        }

        private void extractSegments(CoordinateSequence cs) {
            for (int i = 0; i < cs.size() - 1; i++) {
                segments.add(new Segment(cs.getX(i), cs.getY(i), cs.getX(i + 1), cs.getY(i + 1)));
            }
        }

        List<Polygon> getFaces(GeometryFactory gf, double extrude) {
            // sort the segments from bottom to top
            Collections.sort(segments);

            // extrude each segment
            List<Polygon> result = new ArrayList<>();
            for (Segment segment : segments) {
                CoordinateSequence cs = JTS.createCS(gf.getCoordinateSequenceFactory(), 5, 2);
                cs.setOrdinate(0, 0, segment.x0);
                cs.setOrdinate(0, 1, segment.y0);
                cs.setOrdinate(3, 0, segment.x0);
                cs.setOrdinate(3, 1, segment.y0 + extrude);
                cs.setOrdinate(2, 0, segment.x1);
                cs.setOrdinate(2, 1, segment.y1 + extrude);
                cs.setOrdinate(1, 0, segment.x1);
                cs.setOrdinate(1, 1, segment.y1);
                cs.setOrdinate(4, 0, segment.x0);
                cs.setOrdinate(4, 1, segment.y0);
                result.add(gf.createPolygon(gf.createLinearRing(cs), null));
            }

            return result;
        }
    }

    static class Segment implements Comparable<Segment> {
        double x0, y0;
        double x1, y1;

        public Segment(double x0, double y0, double x1, double y1) {
            this.x0 = x0;
            this.y0 = y0;
            this.x1 = x1;
            this.y1 = y1;
        }

        @Override
        public int compareTo(Segment other) {
            double maxY = Math.max(y0, y1);
            double otherMaxY = Math.max(other.y0, other.y1);
            if (maxY > otherMaxY) return -1;
            if (maxY < otherMaxY) return 1;

            double maxX = Math.max(x0, x1);
            double otherMaxX = Math.max(other.x0, other.x1);
            if (maxX > otherMaxX) return 1;
            if (maxX < otherMaxX) return -1;
            return 0;
        }
    }
}
